using System;
using System.Collections;
using System.Collections.Generic;
using MSOutlook = Microsoft.Office.Interop.Outlook;
using System.Reflection;

namespace Microsoft.Sample.Outlook.Utility
{
	public enum SyncDirection
	{
		ItemToOutlook,
		OutlookToItem
	}

    public sealed class SyncEngine
	{
		public delegate void ProcessOutlookItem(ItemAdapter item);
		public delegate void SyncOutlookFolder<TFolder>(MSOutlook.MAPIFolder outlookFolder, TFolder folderItem)
			where TFolder : IOutlookSyncItem;

		/// <summary>
		/// This method ensures that the folders underneath the specified parent folder 
		/// match the list of folder items in the specified collection. Folders underneath 
		/// the parent that aren't in the collection are deleted. Folders that are in the 
		/// collection are synced by calling the syncFolder delegate
		/// </summary>
		/// <typeparam name="TFolderItem"></typeparam>
		/// <param name="parentFolder"></param>
		/// <param name="folders"></param>
		/// <param name="syncFolder"></param>
		public static void SyncFolders<TFolderItem>(MSOutlook.MAPIFolder parentFolder, ICollection folders, SyncOutlookFolder<TFolderItem> syncFolder)
			where TFolderItem : IOutlookSyncItem
		{

			//Retrieve the OutlookFolderSync attribute from the TFolder type
			OutlookFolderSyncAttribute[] arFolderSync =
			(OutlookFolderSyncAttribute[])typeof(TFolderItem).GetCustomAttributes(
				typeof(OutlookFolderSyncAttribute),
				false);

			if (arFolderSync.Length != 1)
				throw new ApplicationException("Invalid type used for SyncFolders");

			OutlookFolderSyncAttribute folderSync = arFolderSync[0];

			//Save a list of entryID's of items that have been synced w/ the DB
			List<string> listEntryID = new List<string>();

			//Sync each account from the DB
			foreach (TFolderItem folderItem in folders)
			{
				//Get the folder name via the property specified in the Folder sync attribute
				string folderName = (string)folderItem.GetType().InvokeMember(
					folderSync.FolderNameProperty,
					BindingFlags.GetProperty,
					null, folderItem, null);

				//Look for a folder in the current directory that matches the name
				MSOutlook.MAPIFolder outlookFolder = Folder.GetFolder(parentFolder, folderName);

				//If the folder exists in the DB but not in Outlook, create it in Outlook
				if (outlookFolder == null)
				{
					outlookFolder = Folder.AddChildFolder(parentFolder, folderName, folderSync.FolderType);
				}

				//If the folder exists in both the DB and Outlook, check to make sure the
				//entry ids match. If they don't, delete the existing folder and recreate
				else
				{
					if (outlookFolder.EntryID != folderItem.EntryID)
					{
						outlookFolder.Delete();
						outlookFolder = Folder.AddChildFolder(parentFolder, folderName, folderSync.FolderType);
					}
				}

				//Call the supplied delegate to sync the contents of the folder
				syncFolder(outlookFolder, folderItem);

				//Update the database with the folder's entry ID if it doesn't match
				if (outlookFolder.EntryID != folderItem.EntryID)
					folderItem.EntryID = outlookFolder.EntryID;

				//add this folder's entry ID to the list of synced folders
				if (!listEntryID.Contains(outlookFolder.EntryID))
					listEntryID.Add(outlookFolder.EntryID);
			}

			//find the folders in Outlook that don't have a matching account in the DB. 
			List<MSOutlook.MAPIFolder> listFolders = new List<MSOutlook.MAPIFolder>();
			foreach (MSOutlook.MAPIFolder outlookFolder in parentFolder.Folders)
			{
				if (!listEntryID.Contains(outlookFolder.EntryID))
					listFolders.Add(outlookFolder);
			}

			//Delete the folders w/o matching accounts
			foreach (MSOutlook.MAPIFolder outlookFolder in listFolders)
				outlookFolder.Delete();
		}

		/// <summary>
		/// This method forward to the main SyncItems method, passing null for the ProcessOutlookItem
		/// delegate, ensuring that extra outlook items in the folder get deleted
		/// </summary>
		/// <typeparam name="TItem"></typeparam>
		/// <param name="folder"></param>
		/// <param name="items"></param>
		public static void SyncItems<TItem>(MSOutlook.MAPIFolder folder, ICollection items, SyncDirection syncDirection)
			where TItem : IOutlookSyncItem
		{
			SyncItems<TItem>(folder, items, syncDirection, null);
		}

		/// <summary>
		/// This method ensures the items in the folder specified and the items in the collection 
		/// specified are in sync. Items missing from the folder are added. Items existing in the
		/// folder are updated as needed. Items in the folder that aren't in the database are
		/// processed by the calling code via the ProcessOutlookItem delegate. If no delegate is provided, 
		/// the extra items are deleted
		/// </summary>
		/// <typeparam name="TItem"></typeparam>
		/// <param name="folder"></param>
		/// <param name="items"></param>
		/// <param name="processItem"></param>
		public static void SyncItems<TItem>(MSOutlook.MAPIFolder folder, ICollection items, SyncDirection syncDirection, ProcessOutlookItem processItem)
			where TItem : IOutlookSyncItem
		{
			//Get the sync info for the type
			SyncInfo sync = SyncInfo.GetSyncInfo(typeof(TItem));

			//Save a list of entryID's of items that have been synced w/ the DB
			List<string> listEntryID = new List<string>();

			//Iterate thru the items in the collection
			foreach (TItem item in items)
			{
				//Find the item in Outlook
				bool dirty = false;
				ItemAdapter outlookItem = FindItem(item.EntryID, folder, sync.ItemType, ref dirty);

				//Sync the item w/ the values from the database
				dirty |= SyncItem<TItem>(item, outlookItem, sync, syncDirection);

				//If the outlook item has been updated in any way, save it
				if (dirty)
					outlookItem.Save();

				//update the DB with the outlookItem's entryID if it doesn't match the value in 
				//the DB
				string entryID = outlookItem.EntryID;
				if (entryID != item.EntryID)
					item.EntryID = entryID;

				//add this item's entry ID to the list of synced IDs
				listEntryID.Add(item.EntryID);
			}

			//Now need to iterate thru all the items in the folder and do something
			//with the items in the folder that are NOT in the DB. 
			List<ItemAdapter> listUnsyncedItems = new List<ItemAdapter>();
			foreach (object o in folder.Items)
			{
				ItemAdapter outlookItem = ItemAdapter.FromObject(o);
				if (!listEntryID.Contains(outlookItem.EntryID))
					listUnsyncedItems.Add(outlookItem);
			}

			//By default, we delete extra items. But we provide the client app the ability
			//to provide a method (via the processItem delegate) to handle extra items
			//as the caller sees fit
			foreach (ItemAdapter item in listUnsyncedItems)
			{
				if (processItem == null)
					item.Delete();
				else
					processItem(item);
			}
		}

		/// <summary>
		/// private helper method to find the item in the folder with the matching EntryID, creating
		/// the item if it doesn't exist
		/// </summary>
		/// <typeparam name="TItem"></typeparam>
		/// <param name="item"></param>
		/// <param name="folder"></param>
		/// <param name="sync"></param>
		/// <param name="dirty"></param>
		/// <returns></returns>
		private static ItemAdapter FindItem(string entryID, MSOutlook.MAPIFolder folder, MSOutlook.OlItemType itemType, ref bool dirty) 
		{
			if (entryID == null)
			{
				//if the item's entry ID is null, it has never been 
				//synced into Outlook, so create it new
				dirty = true;
				return ItemAdapter.FromObject(folder.Items.Add(itemType));
			}

			//Find the item in Outlook by it's EntryID
			ItemAdapter outlookItem = null;
			try
			{
				outlookItem = ItemAdapter.FromObject(folder.Session.GetItemFromID(entryID, folder.StoreID));
			}
			catch (System.Runtime.InteropServices.COMException)
			{
				//For whatver reason, GetItemFromID throws an exception instead of 
				//just returning null, hence the need to swallow the exception
			}

			if (outlookItem == null)
			{
				//If GetItemFromID couldn't find the item, it must have been
				//deleted in Outlook, so create it new
				dirty = true;
				return ItemAdapter.FromObject(folder.Items.Add(itemType));
			}

			//Make sure the item that GetItemFromId returned exists in the right folder.
			//If not, assume the user moved it for reasons of their own choosing
			//and create it new.
			MSOutlook.MAPIFolder parent = (MSOutlook.MAPIFolder)outlookItem.Parent;

			if (!parent.FullFolderPath.Equals(folder.FullFolderPath))
			{
				dirty = true;
				return ItemAdapter.FromObject(folder.Items.Add(itemType));
			}

			return outlookItem;
		}

		public static bool SyncItem<TItem>(TItem item, ItemAdapter outlookItem, SyncDirection syncDirection)
		{
			//Get the sync info for the type
			SyncInfo sync = SyncInfo.GetSyncInfo(typeof(TItem));

			return SyncItem<TItem>(item, outlookItem, sync, syncDirection);
		}

		/// <summary>
		/// private helper method to sync the properties on the item with the properties on the 
		/// outlook item
		/// </summary>
		/// <typeparam name="TItem"></typeparam>
		/// <param name="item"></param>
		/// <param name="outlookItem"></param>
		/// <param name="sync"></param>
		/// <param name="dirty"></param>
		/// <returns></returns>
		private static bool SyncItem<TItem>(TItem item, ItemAdapter outlookItem, SyncInfo sync, SyncDirection syncDirection)
		{
			bool dirty = false;

			//iterate thru the properties marked witht the OutlookFieldSync 
			//attribute and update the version of each property in Outlook 
			//if it differs from the DB data
			foreach (PropertyInfo prop in sync.SyncProperties.Keys)
			{
				object dbPropValue = prop.GetValue(item, null);
				object olPropValue = GetOutlookItemProperty(outlookItem, sync.SyncProperties[prop]);

				if ((dbPropValue != null && !dbPropValue.Equals(olPropValue)) ||
					(dbPropValue == null && olPropValue != null))
				{
					if (syncDirection == SyncDirection.ItemToOutlook)
						SetOutlookItemProperty(outlookItem, dbPropValue, sync.SyncProperties[prop]);
					else //SyncDirection.OutlookToItem
						prop.SetValue(item, olPropValue, null);

					dirty = true;
				}
			}
			return dirty;
		}

		private static object GetOutlookItemProperty(ItemAdapter outlookItem, OutlookFieldSyncAttribute syncProp)
		{
			if (syncProp.IsUserProp)
			{
				MSOutlook.UserProperty uProp = outlookItem.UserProperties[syncProp.FieldName];

				if (uProp != null)
					return uProp.Value;

				return null;
			}

			return outlookItem.GetProperty(syncProp.FieldName);
		}

		private static void SetOutlookItemProperty(ItemAdapter outlookItem, object value, OutlookFieldSyncAttribute syncProp)
		{
			if (syncProp.IsUserProp)
			{
				MSOutlook.UserProperty uProp = outlookItem.UserProperties[syncProp.FieldName];

				if (uProp == null)
					uProp = outlookItem.UserProperties.Add(syncProp.FieldName, GetPropType(value), true, Missing.Value);

				uProp.Value = value;
			}
			else
				outlookItem.SetProperty(syncProp.FieldName, value);
		}

		private static MSOutlook.OlUserPropertyType GetPropType(object o)
		{
			if (o is bool)
				return MSOutlook.OlUserPropertyType.olYesNo;

			if (o is DateTime)
				return MSOutlook.OlUserPropertyType.olDateTime;

			if (o is string)
				return MSOutlook.OlUserPropertyType.olText;

			throw new ApplicationException("Invalid Property Type " + o.GetType().Name);

		}

	}
}
